# -*- coding: utf-8 -*-

# Copyright (c) 2007 - 2025 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing an exporter for RTF.
"""

# This code is a port of the C++ code found in SciTE 1.74
# Original code: Copyright 1998-2006 by Neil Hodgson <neilh@scintilla.org>

import time

from PyQt6.Qsci import QsciScintilla
from PyQt6.QtGui import QFontInfo

from eric7 import Preferences
from eric7.EricGui.EricOverrideCursor import EricOverrideCursor
from eric7.EricWidgets import EricMessageBox

from .ExporterBase import ExporterBase


class ExporterRTF(ExporterBase):
    """
    Class implementing an exporter for RTF.
    """

    RTF_HEADEROPEN = "{\\rtf1\\ansi\\deff0\\deftab720"
    RTF_HEADERCLOSE = "\n"
    RTF_FONTDEFOPEN = "{\\fonttbl"
    RTF_FONTDEF = "{{\\f{0:d}\\fnil\\fcharset{1:d} {2};}}"
    RTF_FONTDEFCLOSE = "}"
    RTF_COLORDEFOPEN = "{\\colortbl"
    RTF_COLORDEF = "\\red{0:d}\\green{1:d}\\blue{2:d};"
    RTF_COLORDEFCLOSE = "}"
    RTF_INFOOPEN = "{\\info "
    RTF_INFOCLOSE = "}"
    RTF_COMMENT = "{\\comment Generated by eric's RTF export filter.}"
    # to be used by strftime
    RTF_CREATED = r"{\creatim\yr%Y\mo%m\dy%d\hr%H\min%M\sec%S}"
    RTF_BODYOPEN = ""
    RTF_BODYCLOSE = "}"

    RTF_SETFONTFACE = "\\f"
    RTF_SETFONTSIZE = "\\fs"
    RTF_SETCOLOR = "\\cf"
    RTF_SETBACKGROUND = "\\highlight"
    RTF_BOLD_ON = "\\b"
    RTF_BOLD_OFF = "\\b0"
    RTF_ITALIC_ON = "\\i"
    RTF_ITALIC_OFF = "\\i0"

    RTF_EOLN = "\\line\n"
    RTF_TAB = "\\tab "

    RTF_COLOR = "#000000"

    def __init__(self, editor, parent=None):
        """
        Constructor

        @param editor reference to the editor object
        @type QScintilla.Editor.Editor
        @param parent parent object of the exporter
        @type QObject
        """
        ExporterBase.__init__(self, editor, parent)

    def __GetRTFNextControl(self, pos, style):
        """
        Private method to extract the next RTF control word from style.

        @param pos position to start search
        @type int
        @param style style definition to search in
        @type str
        @return tuple of new start position and control word found
        @rtype tuple of (int, str)
        """
        # \f0\fs20\cf0\highlight0\b0\i0
        if pos >= len(style):
            return pos, ""

        oldpos = pos
        pos += 1  # implicit skip over leading '\'
        while pos < len(style) and style[pos] != "\\":
            pos += 1
        return pos, style[oldpos:pos]

    def __GetRTFStyleChange(self, last, current):
        """
        Private method to extract control words that are different between two
        styles.

        @param last least recently used style
        @type str
        @param current current style
        @type str
        @return string containing the delta between these styles
        @rtype str
        """
        # \f0\fs20\cf0\highlight0\b0\i0
        lastPos = 0
        currentPos = 0
        delta = ""
        i = 0
        while i < 6:
            lastPos, lastControl = self.__GetRTFNextControl(lastPos, last)
            currentPos, currentControl = self.__GetRTFNextControl(currentPos, current)
            if lastControl != currentControl:
                delta += currentControl
            i += 1
        if delta != "":
            delta += " "
        return delta

    def exportSource(self):
        """
        Public method performing the export.
        """
        filename = self._getFileName(self.tr("RTF Files (*.rtf)"))
        if not filename:
            return

        self.editor.recolor(0, -1)
        tabs = Preferences.getEditorExporter("RTF/UseTabs")
        tabSize = self.editor.getEditorConfig("TabWidth")
        if tabSize == 0:
            tabSize = 4

        with EricOverrideCursor(), open(filename, "w", encoding="utf-8") as f:
            try:
                styles, fontsize = self.__prepareStyles(f)

                lastStyle = (
                    self.RTF_SETFONTFACE
                    + "0"
                    + self.RTF_SETFONTSIZE
                    + "{0:d}".format(fontsize)
                    + self.RTF_SETCOLOR
                    + "0"
                    + self.RTF_SETBACKGROUND
                    + "1"
                    + self.RTF_BOLD_OFF
                    + self.RTF_ITALIC_OFF
                )

                lengthDoc = self.editor.length()
                prevCR = False
                column = 0
                pos = 0
                deltaStyle = ""
                styleCurrent = -1
                utf8 = self.editor.isUtf8()
                utf8Ch = b""
                utf8Len = 0

                while pos < lengthDoc:
                    ch = self.editor.byteAt(pos)
                    style = self.editor.styleAt(pos)
                    if style != styleCurrent:
                        deltaStyle = self.__GetRTFStyleChange(lastStyle, styles[style])
                        if deltaStyle:
                            f.write(deltaStyle)
                        styleCurrent = style
                        lastStyle = styles[style]

                    if ch == b"{":
                        f.write("\\{")
                    elif ch == b"}":
                        f.write("\\}")
                    elif ch == b"\\":
                        f.write("\\\\")
                    elif ch == b"\t":
                        if tabs:
                            f.write(self.RTF_TAB)
                        else:
                            ts = tabSize - (column % tabSize)
                            f.write(" " * ts)
                            column += ts - 1
                    elif ch == b"\n":
                        if not prevCR:
                            f.write(self.RTF_EOLN)
                            column -= 1
                    elif ch == b"\r":
                        f.write(self.RTF_EOLN)
                        column -= 1
                    else:
                        if ord(ch) > 0x7F and utf8:
                            utf8Ch += ch
                            if utf8Len == 0:
                                if (utf8Ch[0] & 0xF0) == 0xF0:
                                    utf8Len = 4
                                elif (utf8Ch[0] & 0xE0) == 0xE0:
                                    utf8Len = 3
                                elif (utf8Ch[0] & 0xC0) == 0xC0:
                                    utf8Len = 2
                                column -= 1
                                # will be incremented again later
                            elif len(utf8Ch) == utf8Len:
                                ch = utf8Ch.decode("utf8")
                                if ord(ch) <= 0xFF:
                                    f.write("\\'{0:x}".format(ord(ch)))
                                else:
                                    f.write(
                                        "\\u{0:d}\\'{1:x}".format(
                                            ord(ch), ord(ch) & 0xFF
                                        )
                                    )
                                utf8Ch = b""
                                utf8Len = 0
                            else:
                                column -= 1
                                # will be incremented again later
                        else:
                            f.write(ch.decode())

                    column += 1
                    prevCR = ch == b"\r"
                    pos += 1

                f.write(self.RTF_BODYCLOSE)
            except OSError as err:
                EricMessageBox.critical(
                    self.editor,
                    self.tr("Export source"),
                    self.tr(
                        """<p>The source could not be exported to"""
                        """ <b>{0}</b>.</p><p>Reason: {1}</p>"""
                    ).format(filename, str(err)),
                )

    def __prepareStyles(self, f):
        """
        Private method to generate and store the different styles.

        @param f filepointer to the open RTF
        @type object
        @return styles, fontsize
        @rtype dict, int
        """
        styles = {}
        fonts = {}
        colors = {}
        lastStyle = ""

        lex = self.editor.getLexer()

        wysiwyg = Preferences.getEditorExporter("RTF/WYSIWYG")
        if wysiwyg:
            if lex:
                defaultFont = lex.font(QsciScintilla.STYLE_DEFAULT)
            else:
                defaultFont = Preferences.getEditorOtherFonts("DefaultFont")
        else:
            defaultFont = Preferences.getEditorExporter("RTF/Font")
        fontface = defaultFont.family()
        fontsize = QFontInfo(defaultFont).pointSize() << 1
        if fontsize == 0:
            fontsize = 10 << 1
        characterset = QsciScintilla.SC_CHARSET_DEFAULT

        if lex:
            fgColour = lex.color(QsciScintilla.STYLE_DEFAULT)
            bgColour = lex.paper(QsciScintilla.STYLE_DEFAULT)
        else:
            fgColour = self.editor.color()
            bgColour = self.editor.paper()

        f.write(self.RTF_HEADEROPEN + self.RTF_FONTDEFOPEN)
        fonts[0] = fontface
        fontCount = 1
        f.write(self.RTF_FONTDEF.format(0, characterset, fontface))
        colors[0] = fgColour
        colors[1] = bgColour
        colorCount = 2

        if lex:
            istyle = 0
            while istyle <= QsciScintilla.STYLE_MAX:
                if (
                    istyle < QsciScintilla.STYLE_DEFAULT
                    or istyle > QsciScintilla.STYLE_LASTPREDEFINED
                ):
                    if lex.description(istyle):
                        font = lex.font(istyle)
                        lastStyle = self.RTF_SETFONTFACE
                        if wysiwyg:
                            fontKey = None
                            for key, value in fonts.items():
                                if value.lower() == font.family().lower():
                                    fontKey = key
                                    break
                            else:
                                fonts[fontCount] = font.family()
                                f.write(
                                    self.RTF_FONTDEF.format(
                                        fontCount, characterset, font.family()
                                    )
                                )
                                fontKey = fontCount
                                fontCount += 1

                            lastStyle += "{0:d}".format(fontKey)
                        else:
                            lastStyle += "0"

                        lastStyle += self.RTF_SETFONTSIZE
                        if wysiwyg and QFontInfo(font).pointSize():
                            lastStyle += "{0:d}".format(
                                QFontInfo(font).pointSize() << 1
                            )
                        else:
                            lastStyle += "{0:d}".format(fontsize)

                        sColour = lex.color(istyle)
                        sColourKey = None
                        for key, value in colors.items():
                            if value == sColour:
                                sColourKey = key
                                break
                        else:
                            colors[colorCount] = sColour
                            sColourKey = colorCount
                            colorCount += 1
                        lastStyle += self.RTF_SETCOLOR + "{0:d}".format(sColourKey)

                        sColour = lex.paper(istyle)
                        sColourKey = None
                        for key, value in colors.items():
                            if value == sColour:
                                sColourKey = key
                                break
                        else:
                            colors[colorCount] = sColour
                            sColourKey = colorCount
                            colorCount += 1

                        lastStyle += self.RTF_SETBACKGROUND + "{0:d}".format(sColourKey)

                        if font.bold():
                            lastStyle += self.RTF_BOLD_ON
                        else:
                            lastStyle += self.RTF_BOLD_OFF
                        if font.italic():
                            lastStyle += self.RTF_ITALIC_ON
                        else:
                            lastStyle += self.RTF_ITALIC_OFF
                        styles[istyle] = lastStyle

                        # get substyles
                        subs_start, subs_count = self.editor.getSubStyleRange(istyle)
                        for subs_idx in range(subs_count):
                            font = lex.font(subs_start + subs_idx)
                            lastStyle = self.RTF_SETFONTFACE
                            if wysiwyg:
                                fontKey = None
                                for key, value in fonts.items():
                                    if value.lower() == font.family().lower():
                                        fontKey = key
                                        break
                                else:
                                    fonts[fontCount] = font.family()
                                    f.write(
                                        self.RTF_FONTDEF.format(
                                            fontCount, characterset, font.family()
                                        )
                                    )
                                    fontKey = fontCount
                                    fontCount += 1

                                lastStyle += "{0:d}".format(fontKey)
                            else:
                                lastStyle += "0"

                            lastStyle += self.RTF_SETFONTSIZE
                            if wysiwyg and QFontInfo(font).pointSize():
                                lastStyle += "{0:d}".format(
                                    QFontInfo(font).pointSize() << 1
                                )
                            else:
                                lastStyle += "{0:d}".format(fontsize)

                            sColour = lex.color(subs_start + subs_idx)
                            sColourKey = None
                            for key, value in colors.items():
                                if value == sColour:
                                    sColourKey = key
                                    break
                            else:
                                colors[colorCount] = sColour
                                sColourKey = colorCount
                                colorCount += 1
                            lastStyle += self.RTF_SETCOLOR + "{0:d}".format(sColourKey)

                            sColour = lex.paper(subs_start + subs_idx)
                            sColourKey = None
                            for key, value in colors.items():
                                if value == sColour:
                                    sColourKey = key
                                    break
                            else:
                                colors[colorCount] = sColour
                                sColourKey = colorCount
                                colorCount += 1

                            lastStyle += self.RTF_SETBACKGROUND + "{0:d}".format(
                                sColourKey
                            )

                            if font.bold():
                                lastStyle += self.RTF_BOLD_ON
                            else:
                                lastStyle += self.RTF_BOLD_OFF
                            if font.italic():
                                lastStyle += self.RTF_ITALIC_ON
                            else:
                                lastStyle += self.RTF_ITALIC_OFF
                            styles[subs_idx - subs_start] = lastStyle

                    else:
                        styles[istyle] = (
                            self.RTF_SETFONTFACE
                            + "0"
                            + self.RTF_SETFONTSIZE
                            + "{0:d}".format(fontsize)
                            + self.RTF_SETCOLOR
                            + "0"
                            + self.RTF_SETBACKGROUND
                            + "1"
                            + self.RTF_BOLD_OFF
                            + self.RTF_ITALIC_OFF
                        )

                istyle += 1
        else:
            styles[0] = (
                self.RTF_SETFONTFACE
                + "0"
                + self.RTF_SETFONTSIZE
                + "{0:d}".format(fontsize)
                + self.RTF_SETCOLOR
                + "0"
                + self.RTF_SETBACKGROUND
                + "1"
                + self.RTF_BOLD_OFF
                + self.RTF_ITALIC_OFF
            )

        f.write(self.RTF_FONTDEFCLOSE + self.RTF_COLORDEFOPEN)
        for value in colors.values():
            f.write(self.RTF_COLORDEF.format(value.red(), value.green(), value.blue()))
        f.write(self.RTF_COLORDEFCLOSE)
        f.write(self.RTF_INFOOPEN + self.RTF_COMMENT)
        f.write(time.strftime(self.RTF_CREATED))
        f.write(self.RTF_INFOCLOSE)
        f.write(
            self.RTF_HEADERCLOSE
            + self.RTF_BODYOPEN
            + self.RTF_SETFONTFACE
            + "0"
            + self.RTF_SETFONTSIZE
            + "{0:d}".format(fontsize)
            + self.RTF_SETCOLOR
            + "0 "
        )

        return styles, fontsize


def createExporter(editor, parent=None):
    """
    Function to instantiate an exporter object.

    @param editor reference to the editor object
    @type QScintilla.Editor.Editor
    @param parent parent object of the exporter (defaults to None)
    @type QObject (optional)
    @return exporter object
    @rtype ExporterRTF
    """
    return ExporterRTF(editor, parent=parent)
